| 함수 | 출처 패키지 | 역할 | 주요 논항 및 예시 |
|---|---|---|---|
| map_dfr() / map() | tidyverse(purrr) | 리스트의 각 요소에 함수를 적용하고 결과를(DF/리스트로) 반환 | map(charvec, ~kiwi_tokenizer_single(.x, kiwi_analyzer)) |
| corpus() | quanteda | 텍스트 데이터와 메타데이터를 말뭉치(corpus) 객체로 변환 | corpus(데이터, text_field = “텍스트열”, docid_field = “ID열”) |
| docvars() | quanteda | 말뭉치 객체에 메타데이터(정답 레이블 등)를 저장/호출 | docvars(말뭉치, “변수명”) <- 값 |
| dfm() | quanteda | 토큰화 결과를 문서-단어 행렬(DFM)로 변환 | dfm(토큰_객체) |
| dfm_tfidf() | quanteda | DFM에 TF-IDF 가중치를 적용하여 단어 중요도 보정 | dfm_tfidf(DFM_객체) |
| pam() | cluster | K-Medoids 군집화(PAM 알고리즘) 실행 | pam(숫자_행렬, k = 3) |
| fviz_nbclust() | factoextra | 최적의 군집 수(K)를 실루엣 계수 등으로 시각화 | fviz_nbclust(데이터, FUNcluster = pam, method = “silhouette”) |
| fviz_cluster() | factoextra | 군집화 결과를 2D 평면에 시각화 | fviz_cluster(pam_결과_객체) |
| table() | Base R | 모델 예측과 실제 정답을 비교하는 교차표(혼동행렬) 생성 | table(True = 실제값, Model = 예측값) |
| PCA() | FactoMineR | 주성분 분석(PCA) 실행 | PCA(숫자_행렬, scale.unit = TRUE, graph = FALSE) |
| fviz_eig() | factoextra | PCA의 Scree Plot을 시각화(각 PC의 분산 설명력) | fviz_eig(PCA_결과_객체) |
| fviz_contrib() | factoextra | PCA 축(PC)에 기여한 변수(단어)를 시각화(축 명명용) | fviz_contrib(PCA_결과, choice = “var”, axes = 1) |
| fviz_pca_ind() / fviz_pca_var() | factoextra | PCA 공간에 개별 관측치(문서) 또는 변수(단어)를 시각화 | fviz_pca_ind(PCA_결과, habillage = 정답_팩터) |
pam())의 원리와 장단점을 비교
설명할 수 있으며, Silhouette 계수를 활용해 객관적인
K(군집 수)를 선택할 수 있음.# 패키지 설치
# ----------------------------------------------------
# cluster: PAM(K-Medoids), Silhouette 등 표준 군집화 도구.
# FactoMineR: PCA 및 해석을 위한 강력한 도구.
# factoextra: cluster, FactoMineR로 얻은 결과를 ggplot2로 시각화.
# ----------------------------------------------------
install.packages(c("cluster", "FactoMineR", "factoextra"))
# 패키지 로드
library(tidyverse)
library(quanteda) # 텍스트 분석 핵심 패키지(corpus, dfm, dfm_tfidf, tokens_custom 등).
library(jsonlite) # fromJSON() 함수로 JSON 파일 로드.
library(reticulate) # R에서 Python을 사용하기 위한 패키지.
library(furrr) # purrr와 동일한 기능을 제공하되 병렬처리가 가능하도록 해주는 패키지.
library(stringi) # 문자열을 UTF-8 형식으로 포맷해주는 함수를 탑재한 패키지.
library(cluster) # 군집화(K-Medoids) 기능을 제공해주는 패키지.
library(FactoMineR) # 주성분 분석(PCA) 기능을 제공해주는 패키지.
library(factoextra) # 군집화 및 주성분 분석 관련 시각화 기능을 제공해주는 패키지.
# Conda 가상환경 지정
# 이 가상환경에는 한국어 형태소 분석기 'kiwipiepy'가 미리 설치되어 있어야 함.
use_condaenv("my_env")
# Python 형태소 분석기 모듈 불러오기
# 'kiwipiepy' 라이브러리를 Python에서 import하여 'kiwi_analyzer'라는 R 객체에 할당.
# 이제 'kiwi_analyzer' 객체를 통해 R에서 'kiwipiepy'의 함수들을 호출할 수 있게 됨.
kiwi <- import("kiwipiepy")$Kiwi()
# [1] 단일 텍스트 처리 함수 정의
# 텍스트 1개를 입력받아 전처리 후 토큰화된 벡터를 반환.
kiwi_tokenizer <- function(text) {
# Python 객체의 $analyze() 메서드를 호출하여 형태소 분석 실행
results <- kiwi$analyze(text)
#(오류 방지) kiwipiepy가 빈 결과를 반환할 경우, 빈 문자열 벡터를 반환
if(length(results) == 0 || length(results[[1]]) == 0) {
return(character(0))
}
# map_dfr: purrr 함수. 리스트(results[[1]][[1]])의 각 요소를(.x) 순회하며
# tibble(morph = .x$form, pos = .x$tag)을 실행하고,
# 그 결과를 모두 합쳐 하나의 tibble로 만듦.
tagged_tokens <- map_dfr(results[[1]][[1]], ~tibble(
morph = .x$form, # 형태소
pos = .x$tag # 품사
))
# "형태소/품사" 기반의 도메인 특화 불용어 사전 정의
stop_words_pos <- c("이/VCP", "것/NNB", "곳/NNG", "곳/NNB", "하/VX", "하/VV", "수/NNB", "되/VV", "듯/NNB",
"하/XSA", "들/XSN", "되/XSA", "되/XSV", "하/XSV", "이/VCP", "이/VV", "적/XSN",
"님/NNG", "고객/NNG", "상담사/NNG", "상담원/NNG", "저희/NP", "예/IC", "지금/MAG",
"그/MM", "보/VV", "보/VX")
# 토큰 필터링하기
token_vector <- tagged_tokens %>%
# filter()와 정규표현식(str_detect)으로 불필요한 품사 제거
filter(!str_detect(pos, "^J"), # J로 시작하는 조사(JKS, JKC...) 제거.
!str_detect(pos, "^E"), # E로 시작하는 어미(EF, EP...) 제거.
!str_detect(pos, "SF|SP|SS"), # 구두점(마침표, 쉼표 등) 제거.
!str_detect(morph, "\\#[ㄱ-ㅎ가-힣]{1,20}|반갑습니다") # 개인신상을 특정할 수 있는 고유명사 대신 붙은 "#"이 앞에 있는 어절과 인사 제거.
) %>%
# mutate(): "형태소/품사" 형태의 새 열 생성([EX] "배송/NNG")
mutate(tagged_morph = str_c(morph, "/", pos)) %>%
# 위에서 정의한 불용어 사전에 포함되지 않는(! %in%) 토큰만 남김
filter(!tagged_morph %in% stop_words_pos) %>%
# pull(): tibble의 'morph' 열만 추출하여 R의 기본 문자열 '벡터'로 변환
pull(morph)
# 최종 정제된 토큰 벡터 반환
return(token_vector)
}
library(googledrive) # 구글 드라이브에 저장된 파일 공유 링크 통해 다운로드하는 데 필요한 패키지 불러오기.
# 인증 해제하기(공유된 공개 파일일 경우).
drive_deauth()
# 다운로드할 파일을 저장할 폴더 만들기.
dir.create("ML_text/unsupervised", recursive = T)
# "ML_text_JSON.zip" 압축파일에 대한 구글 드라이브 공유 링크 고유번호 저장하기.
ml_url_id <- "1adY41zg_s05N-k9ndlK34M4O8c_PZr1a"
# 내보낼 로컬 경로 지정하기.
local_path <- "ML_text/unsupervised/unsupervised_dataset.zip"
# 파일 다운로드하기.
drive_download(as_id(ml_url_id), path = local_path, overwrite = TRUE)
# 다운로드한 압축파일 ML_text/unsupervised 폴더에 압축 풀기.
unzip(local_path, exdir = "ML_text/unsupervised")
# JSON 파일 목록 불러오기.
unsupervised.list <- list.files(path = "ML_text/unsupervised", pattern = "json$", recursive = T)
unsupervised.list.1 <- str_c("ML_text/unsupervised/", unsupervised.list)
# 문제가 있는 파일 걸러내기 & 필요한 칼럼만 추출하기.
safe_read_json <- function(file) {
tryCatch({
json <- fromJSON(file)
tibble(
file = basename(file),
identifier = json$dataset$identifier,
doc_id = json$dataset$name,
true_topic = json$info$annotations$subject,
text = json$info$annotations$text,
lines = json$info$annotations$lines
)
}, error = function(e) {
message(" Error in file: ", file)
return(NULL)
})
}
# map_dfr 함수를 사용하여 목록 내 파일 일괄적으로 읽어들이기.
unsupervised.list.2 <- map_dfr(unsupervised.list.1, safe_read_json)
# lines 슬롯의 화자(speaker)별 화행(speechAct) 정보 수집하기.
speaker_act_summary <- unsupervised.list.2$lines %>%
map_chr(~ .x %>%
as_tibble() %>% # lines 슬롯을 티블로 변환하기.
mutate(speaker_act_match = str_c(speaker$id, "_", speechAct), # speaker(화자)와 speechAct(화행)을 결합하기([EX] A_질문하기, B_부탁하기).
speaker_act_match = str_remove_all(speaker_act_match, " ")) %>% # 화행 기술 내 띄어쓰기 모두 제거하기.
summarise(speaker_act_match = str_c(speaker_act_match, collapse = " ")) %>% # 하나의 대화에 포함된 모든 말 차례의 화행을 띄어쓰기 단위로 통합하기.
pull(speaker_act_match) # 하나의 대화에 포함된 모든 화행을 벡터 형식으로 반출하기.
) %>%
as_tibble() %>% # 대화별 화행 통합 정보 벡터를 티블로 변환하기.
rename(speech_act = value) # value라는 제목의 열 제목을 speech_act로 바꾸기.
# unsupervised.list.2 티블과 speaker_act_summary 티블 열 단위로 결합하기.
unsupervised.list.2.1 <- bind_cols(unsupervised.list.2, speaker_act_summary)
# 5개 주제에 대해 각 1000개의 대화를 랜덤 추출하기.
set.seed(123)
unsupervised.list.3 <- unsupervised.list.2.1 %>%
filter(true_topic %in% c("AS문의", "제품/사용문의", "배송", "주문/결제", "환불/반품/교환")) %>%
group_by(true_topic) %>%
sample_n(1000) %>%
ungroup()
# 필요한 칼럼만 취하기.
unsupervised.dataset <- unsupervised.list.3 %>%
select(doc_id, true_topic, text, speech_act) # 문서 식별자, 실제 대화주제, 대화 텍스트, 말 차례별 화행 정보.
unsupervised.dataset의 텍스트 열 토큰화하기.unsupervised.dataset_tokenized <- unsupervised.dataset %>%
mutate(text = stri_enc_toutf8(text), # 한글이 깨지지 않도록 UTF-8 형식으로 텍스트 열 포맷하기.
text = future_map(text, kiwi_tokenizer), # 병렬처리를 가능하게 해주는 furrr 패키지의 future_map() 함수 사용하여 text 열에 대해 kiwi_tokenizer() 함수 적용하기.
text = future_map_chr(text, ~str_c(.x, collapse = " ")) # 병렬처리를 가능하게 해주는 furrr 패키지의 future_map_chr() 함수 사용하여 text 열 벡터에 대해 띄어쓰기 단위로 모든 토큰 이어주기.
)
save(unsupervised.dataset_tokenized, file="unsupervised.dataset_tokenized.rda")
library(googledrive) # 구글 드라이브에 저장된 파일 공유 링크 통해 다운로드하는 데 필요한 패키지 불러오기.
# 인증 해제하기(공유된 공개 파일일 경우).
drive_deauth()
# "unsupervised_learning.zip" 압축파일에 대한 구글 드라이브 공유 링크 고유번호 저장하기.
ml_url_id <- "1LqJIi9WSzrYV4qL-4bUgI9oRgfNio46E"
# 내보낼 로컬 경로 지정하기.
local_path <- "unsupervised_learning.zip"
# 파일 다운로드하기.
drive_download(as_id(ml_url_id), path = local_path, overwrite = TRUE)
# 다운로드한 압축파일 워킹 디렉토리에 압축 풀기.
unzip(local_path, exdir = ".")
docvars()의 핵심기능: quanteda의
corpus 객체에 ‘문서변수(document variables)’, 즉
메타데이터를 할당하거나(<-) 추출함.true_topic)를 corpus 객체 안에
저장함.docvars(코퍼스 객체): 모든 문서변수를 데이터프레임
형태로 추출함.docvars(코퍼스 객체, "변수명"): 특정 “변수명”에
해당하는 벡터를 추출함.docvars(코퍼스 객체, "새 변수명") <- 벡터: 코퍼스에
“새 변수명”으로 벡터를 할당함.load("unsupervised.dataset_tokenized.rda")
# Corpus: 텍스트 분석의 시작점. 텍스트와 메타데이터를 함께 관리하는 객체.
uns_corpus <- corpus(
unsupervised.dataset_tokenized, # 입력 데이터(tibble 또는 data.frame)
docid_field = "doc_id", # 문서 ID로 사용할 열 이름
text_field = "text" # 실제 텍스트 내용이 담긴 열 이름
)
# 메타데이터(정답)
# docvars()(document variables) 함수로 Corpus에 메타데이터(정답 주제)를 저장.
# 나중에 모델 검증용으로 사용할 예정.
docvars(uns_corpus, "true_topic") <- unsupervised.dataset_tokenized$true_topic
tokens(): Corpus 객체를 토큰화해주기.# tokens 생성
uns_tokens <- tokens(uns_corpus, remove_punct = TRUE) # quanteda Corpus 객체 투입 & 최소한의 토큰화 수행.
dfm(): 토큰화가 완료된 ‘tokens’ 객체를 DFM(문서-단어
행렬)으로 만들기.# DFM(문서-단어 행렬) 생성
# 전처리/토큰화가 완료된 'tokens' 객체를 dfm() 함수에 전달하여 DFM 생성.
uns_dfm <- dfm(uns_tokens)
# 말뭉치 등장 빈도 최저한도 설정
uns_dfm_trimmed_min <- dfm_trim(uns_dfm,
min_termfreq = 5, # 전체에서 최소 5회 이상 등장.
min_docfreq = 2, # 최소 2개 문서 이상 등장.
docfreq_type = "count")
# 문서의 성격을 고려하여 trimming하기
# 'A', 'B', '고객', '상담사', '#입니다' 같은 불용어들을 걸러주기.
uns_dfm_trimmed_final <- dfm_trim(
uns_dfm_trimmed_min,
# 전체 문서의 70%를 초과하여 등장하는 단어는 제거.
# (너무 보편적이어서 IDF가 0에 가까워지는 단어들을 제거)
max_docfreq = 0.70, # 70% 이상 문서에 등장한 단어 제거.
docfreq_type = "prop"
)
# DFM 객체 확인
uns_dfm_trimmed_final
## Document-feature matrix of: 5,000 documents, 2,684 features (98.48% sparse) and 2 docvars.
## features
## docs 이어폰 블루투스 연결 잘 안 혹시 어떤 기종 사용 중
## s1_20211104_0001_3642_01 1 2 4 2 3 2 1 1 2 1
## s1_20210914_0001_0729_01 0 0 0 2 5 3 0 0 1 0
## s1_20210913_0001_0393_01 0 0 0 0 0 0 0 0 0 0
## s1_20211008_0006_0076_01 0 0 0 0 0 0 0 0 0 0
## s1_20210916_0002_0099_01 0 0 0 1 1 0 0 0 0 0
## s1_20210916_0002_0211_01 0 0 0 0 0 0 0 0 0 0
## [ reached max_ndoc ... 4,994 more documents, reached max_nfeat ... 2,674 more features ]
as.matrix()
함수를 사용해 R의 기본행렬(matrix) 형태로 변환.tfidf_matrix가 K-Medoids, PCA가 입력받을 우리의
최종 숫자 데이터임!# TF-IDF 변환
# dfm_tfidf() 함수로 DFM을 TF-IDF 가중치 행렬로 변환.
tfidf_dfm <- dfm_tfidf(uns_dfm_trimmed_final)
# 기본행렬(matrix) 포맷으로 변환
# 'cluster', 'FactoMineR' 등 외부 패키지는 DFM 객체를 직접 받지 못하는 경우가 많음.
# as.matrix()를 사용해 R의 기본행렬(matrix) 형태로 변환함.
tfidf_matrix <- as.matrix(tfidf_dfm)
save(tfidf_matrix, file="tfidf_matrix.rda")
# scaling을 통해 단어들의 TF-IDF 값들을 표준화해줌.
tfidf_matrix_scaled <- scale(as.matrix(tfidf_dfm))
save(tfidf_matrix_scaled, file="tfidf_matrix_scaled.rda")
# 'speech_act' 열을 공백(" ") 기준으로 분리하여 개별 행으로 만들기.
speech_acts_long <- unsupervised.dataset_tokenized %>%
# ([EX] "A_인사하기 B_진술하기" -> "A_인사하기" [1행], "B_진술하기" [2행])
separate_rows(speech_act, sep = " ") %>%
# 분리 과정에서 생길 수 있는 빈 문자열 행은 제거하기.
filter(speech_act != "")
# --- [ max_docfreq = 0.7 룰 적용 시작 ] ---
# 전체 문서의 총 수 계산
n_total_docs <- n_distinct(speech_acts_long$doc_id)
# 각 화행(speechAct)이 몇 개의 '고유한' 문서에 등장했는지(docfreq) 계산
speech_acts_docfreq <- speech_acts_long %>%
# doc_id와 speech_act가 중복된 행을 하나로 합치기(문서당 1회만 카운트).
distinct(doc_id, speech_act) %>%
# speech_act를 기준으로 카운트.
count(speech_act, name = "docfreq")
# 전체 문서의 70%를 '초과'하여 등장하는 '너무 흔한 화행' 목록 만들기
common_speech_acts <- speech_acts_docfreq %>%
# [EX] A_진술하기, A_인사하기 등이 여기에 해당될 것.
filter(docfreq / n_total_docs > 0.7) %>%
pull(speech_act) # 'A_진술하기' 같은 화행 이름만 벡터로 추출.
# 'speech_acts_long' 원본에서 '너무 흔한 화행' 제거
speech_acts_filtered <- speech_acts_long %>%
filter(!speech_act %in% common_speech_acts)
# --- [ max_docfreq = 0.7 룰 적용 종료 ] ---
# 필터링된 데이터를 'doc_id'와 'speech_act' 기준으로 카운트.
speech_acts_counted_filtered <- speech_acts_filtered %>%
count(doc_id, speech_act)
# pivot_wider() 함수를 사용해 DFM(문서-용어 행렬) 형태로 펼치기
speech_act_dfm_filtered <- speech_acts_counted_filtered %>%
pivot_wider(
names_from = speech_act, # 새 컬럼의 이름이 될 값들.
values_from = n, # 셀에 채워질 값
values_fill = 0 # 해당 화행이 없는 문서는 0으로 채움
) %>%
select(-`A_N/A`, -`B_N/A`) # 화행 정보가 없는 열은 제거.
# (최종) TF-IDF 행렬과 cbind()하기 좋도록 matrix 객체로 변환
speech_act_matrix <- speech_act_dfm_filtered %>%
# 'doc_id' 컬럼을 행 이름으로 변환.
column_to_rownames(var = "doc_id") %>%
# 데이터프레임을 행렬로 변환.
as.matrix()
# 흔한 화행([EX] 'A_진술하기' 등)이 빠졌는지 확인
colnames(speech_act_matrix)
## [1] "A_부탁하기" "B_감사하기"
## [3] "B_인사하기" "A_감사하기"
## [5] "B_주장하기" "A_약속하기(개인적수준의서약)"
## [7] "B_명령하기/요구하기" "B_부탁하기"
## [9] "A_명령하기/요구하기" "B_약속하기(개인적수준의서약)"
## [11] "A_부정감정표현하기(비난하기포함)" "B_긍정감정표현하기(칭찬하기포함)"
## [13] "A_사과하기" "B_부정감정표현하기(비난하기포함)"
## [15] "B_사과하기" "A_반박하기"
## [17] "B_반박하기" "A_주장하기"
## [19] "A_충고하기" "A_거절하기"
## [21] "B_거절하기" "A_긍정감정표현하기(칭찬하기포함)"
## [23] "B_충고하기"
# 화행 정보 행렬 행 수 확인
nrow(speech_act_matrix) # 4730행(5000행이 안 됨).
## [1] 4730
load("tfidf_matrix.rda")
# tfidf_matrix를 티블로 변환하고 문서 식별자 행 이름을 rowname 열로 추가하기.
tfidf_matrix.tb <- tfidf_matrix %>%
as.data.frame() %>%
rownames_to_column() %>%
as_tibble()
# speech_act_matrix를 티블로 변환하고 문서 식별자 행 이름을 rowname 열로 추가하기.
speech_act_matrix.tb <- speech_act_matrix %>%
as.data.frame() %>%
rownames_to_column() %>%
as_tibble()
# speech_act_matrix.tb 티블의 문서 식별자 열과 tfidf_matrix.tb 티블을 조인시켜
# tfidf_matrix.tb 티블을 speech_act_matrix.tb과 동일한 행 수로 맞추기.
tfidf_matrix.tb.1 <- speech_act_matrix.tb[, 1] %>%
left_join(tfidf_matrix.tb, by = "rowname") %>% # 'rowname' 열을 기준으로 두 티블을 병합하기.
rename(doc_id = rowname) # 'rowname' 열을 'doc_id'이라는 이름으로 바꾸기.
# speech_act_matrix에 행 수를 맞춘 tfidf_matrix_hybrid 행렬 생성하기.
tfidf_matrix_hybrid <- tfidf_matrix.tb.1 %>%
# 'doc_id' 열을 행 이름으로 변환.
column_to_rownames(var = "doc_id") %>%
# 데이터프레임을 행렬로 변환.
as.matrix()
nrow(tfidf_matrix_hybrid)
## [1] 4730
# tfidf_matrix_hybrid 행렬의 행 수와 true_topics 개수 일치시키기.
true_topics_hybrid <- tfidf_matrix.tb.1 %>%
left_join(unsupervised.dataset_tokenized[, 1:2], by ="doc_id") %>% # 'doc_id' 열을 기준으로 tfidf_matrix.tb.1과 unsupervised.dataset_tokenized[ , 1:2] 티블 병합하기.
pull(true_topic) # true_topic 벡터만 추출하기.
tfidf_matrix)을 사용해 비슷한
’주제’의 문서를 K-Medoids(pam())로 묶음.[그림 1] 엘보우 기법 예시
[그림 2] K-중앙값 군집화 예시
pam())는 TF-IDF 같은 고차원 희소 데이터에 두 가지
큰 강점을 가짐.
pam())를 사용함.pam()pam()은 ’실제
데이터’(medoid)를 중심으로 사용하므로 더 강건(robust)하고 해석이 용이함.
tfidf_matrix를 입력받아 문서군집을 생성함.x: 숫자 행렬 또는 data.frame(본 수업에서는
tfidf_matrix가 입력됨).k: 군집의 수(정수). fviz_nbclust()로 찾은
최적의 K 값을 전달함.metric: 사용할 거리 계산법. 기본값은
"euclidean"(유클리드 거리)임.stand: TRUE로 설정 시, pam()
함수가 내부적으로 데이터를 표준화함.pam 객체를 반환함. 이 중
$clustering(각 문서가 할당된 군집번호 벡터)과
$medoids(중심점으로 선택된 문서의 행 이름 또는 인덱스)가
가장 중요함.fviz_nbclust()x: 숫자 행렬. \(\Rightarrow\) 본 수업에서는
tfidf_matrix가 입력됨.FUNcluster: 사용할 군집함수를 지정함([EX]
pam, kmeans, hcut 등).method: K를 평가할 방법을 지정함.
"silhouette"(본 수업 사용, 객관적 지표),
"wss"(엘보우 기법), "gap_stat"(Gap
통계량).k.max: 테스트할 최대 K 값(너무 크면 시간이 오래
걸림).fviz_cluster()pam() 또는 kmeans() 등으로
생성된 군집화 ’결과’를 2D 평면(PC1, PC2)에 시각화함.pam_result 객체를 입력받아,
고차원(수천 개 단어) 공간에서 문서들이 어떻게 그룹화되었는지 2D로
시각화하여 직관적으로 파악함(내부적으로 PCA 또는 MDS를 사용해 2D로 자동
축소함).object: 군집결과 객체(본 수업에서는
pam_result가 입력됨).data: 원본 데이터(tfidf_matrix).
geom="text"로 문서 ID를 표시할 때 필요할 수 있음.ellipse.type: 군집 주변에 타원을 그리는 방식([EX]
"convex"(볼록 껍질), "t", "norm",
"euclid").ggtheme: ggplot2 테마([EX]
theme_bw(), theme_minimal()).# 관련 패키지 로드
library(cluster) # pam() 함수 제공.
library(factoextra) # fviz_nbclust(), fviz_cluster() 시각화 함수 제공.
load("uns_nbclust.rda")
# 최적의 K 탐색
# fviz_nbclust: K-Means, PAM 등 다양한 군집 알고리즘에 대해
# Elbow(wss), Silhouette, Gap-statistic 세 가지 방법으로 최적의 K를 시각화.
uns_nbclust <- fviz_nbclust(
tfidf_matrix_scaled, # 입력 데이터(숫자 행렬).
FUNcluster = pam, # 사용할 군집 알고리즘 지정(K-Medoids).
method = "silhouette", # K 평가 방법(실루엣 계수).
k.max = 5 # 테스트할 최대 K 값(실제 대화유형이 5개이므로 5로 설정).
)
uns_nbclust
pam() 실행 및 시각화# PAM 모델 실행
# cluster 패키지의 pam() 함수로 K-Medoids 군집화 실행
pam_result_k3 <- pam(tfidf_matrix, # 입력 데이터(숫자 행렬)
k = 3 # 앞서 찾은 최적의 K 값(3개)
)
# 군집 할당 결과 확인
# 결과객체의 $clustering 슬롯에 각 문서가 할당된 군집번호가 저장됨.
pam_result_k3$clustering
# 군집 시각화
# fviz_cluster: factoextra의 군집 시각화 함수
# 내부적으로 PCA 또는 MDS를 실행하여 고차원 데이터를 2D로 축소 후 시각화.
uns_cluster_k3 <- fviz_cluster(
pam_result_k3, # PAM 모델 결과 객체
data = tfidf_matrix, # 원본 데이터(필수는 아님)
ellipse.type = "convex", # 군집을 볼록 껍질(convex hull)로 표시
ggtheme = theme_bw() # 그래프 테마
)
ggsave("uns_cluster_k3_plot.png", uns_cluster_k3, width = 10, height = 10)
uns_cluster_k3
Dim1(x축)과 Dim2(y축)의 정체: “요약된
축”Dim1과 Dim2가 가리키는 것
Dim1: 제1 주성분(PC1).Dim2: 제2 주성분(PC2).fviz_cluster() 함수 pam_result_k3을
시각화하기 위해, tfidf_matrix를 2D(2차원)로 그려야 함.
하지만 tfidf_matrix는 (예를 들어) 수천 개의 단어(feature)로
이루어진 수천 차원의 초고차원 공간임. \(\Rightarrow\) fviz_cluster는
이 수천 차원의 데이터를 2D로 ’압축 요약’하기 위해 내부적으로
PCA를 실행함.
Dim1(x축): 2만 단어 차원에 흩어진 전체 정보(분산)를
가장 많이(1순위로) 요약하는 새로운 ‘가상의
축’(PC1)임.Dim2(y축): Dim1이 설명하지 못한
나머지 정보 중에서 가장 많이(2순위로)
요약하는, Dim1과 직교하는(90도로 만나는 = 서로
간에 상관이 존재하지 않는) ‘가상의 축’(PC2)임.Dim1, Dim2 숫자(0.6%, 0.4%)의 의미Dim1(0.6%): “x축(PC1)은 원본 TF-IDF 행렬이 가진 전체
정보(분산)의 단 0.6%만을 설명(요약)하고 있음.”Dim2(0.4%): “y축(PC2)은 전체 정보의 단
0.4%만을 설명하고 있음.”Dim1(x축)은 수천 개 단어의 정보를 요약한 ‘제1
수직선(PC1)’.Dim2(y축)는 수천 개 단어의 정보를 요약한 ‘제2
수직선(PC2)’.x=0(평균)에서 멀어질수록, 해당 문서가 그 축의 ’특성’을
더 강하게 갖는다는 뜻.Dim1)의 -10, -20이라는 숫자는 원점(0)에서 ’Dim1
축의 음(-)의 방향’으로 얼마나 멀리 떨어져 있는지를 나타내는 좌표
값(PC 점수)임.fviz_pca_var(),
fviz_contrib()를 통해 Dim1(x축)의 정체성을
다음과 같이 파악했다고 가정해보자.
Dim1의 양(+)의 방향(x축의 오른쪽): ‘배송’, ‘출발’,
‘기사’ 단어들이 강하게 기여함.Dim1의 음(-)의 방향(x축의 왼쪽): ‘환불’, ‘입금’, ‘취소’
단어들이 강하게 기여함.s1_20210913_0001_022)
Dim1이
아닌 Dim2(y축)의 점수로 그 특성을 파악해야 함.Dim1이라는 새로운
수직선 위에 어디쯤 위치하는지를 나타내는 주성분 점수(principal
component score)임.fviz_nbclust)는
k=3이 최적이라고(수학적으로 잘 분리된다고) 했는데, 왜 이
플롯에서는 3개의 군집이 완전히 겹쳐 보이는가?”Dim1, Dim2)에
비춘 그림자임. 바닥에 비친 그림자만 보면, 3개의
모기떼는 높이(Dim3) 정보가 사라진 채 모두 겹쳐서 하나의 거대한
그림자로 보일 것.fviz_cluster 플롯의 거대한 중첩은 pam()
모델이 군집화에 ’실패’했다는 뜻이 아님.# PAM 모델 실행
# cluster 패키지의 pam() 함수로 K-Medoids 군집화 실행
pam_result_k5 <- pam(tfidf_matrix, # 입력 데이터(숫자 행렬)
k = 5 # 데이터셋의 실제 주제 개수 K 값(5개)
)
# 군집 할당 결과 확인
# 결과객체의 $clustering 슬롯에 각 문서가 할당된 군집번호가 저장됨.
pam_result_k5$clustering
# 군집 시각화
# fviz_cluster: factoextra의 군집 시각화 함수
# 내부적으로 PCA 또는 MDS를 실행하여 고차원 데이터를 2D로 축소 후 시각화.
uns_cluster_k5 <- fviz_cluster(
pam_result_k5, # PAM 모델 결과 객체
data = tfidf_matrix, # 원본 데이터(필수는 아님)
ellipse.type = "convex", # 군집을 볼록 껍질(convex hull)로 표시
ggtheme = theme_bw() # 그래프 테마
)
ggsave("uns_cluster_k5_plot.png", uns_cluster_k5, width = 10, height = 10)
uns_cluster_k5
# tfidf_matrix 행렬과 화행정보 speech_act_matrix 행렬을 열 단위로 합치기.
hybrid_uns_matrix <- cbind(tfidf_matrix, speech_act_matrix)
# 주의사항: 스케일링 필수!
# TF-IDF 값과 화행 빈도 값의 범위를 통일
hybrid_matrix_scaled <- scale(hybrid_uns_matrix)
# PAM 모델 실행
# cluster 패키지의 pam() 함수로 스케일링된 하이브리드 행렬을 사용하여 K-Medoids 군집화 실행
pam_result_hybrid <- pam(hybrid_matrix_scaled, k = 5)
# 군집 할당 결과 확인
# 결과객체의 $clustering 슬롯에 각 문서가 할당된 군집번호가 저장됨.
pam_result_hybrid$clustering
# 군집 시각화
# fviz_cluster: factoextra의 군집 시각화 함수
# 내부적으로 PCA 또는 MDS를 실행하여 고차원 데이터를 2D로 축소 후 시각화.
uns_hybrid_cluster_k5 <- fviz_cluster(
pam_result_hybrid, # PAM 모델 결과 객체
data = hybrid_matrix_scaled, # 원본 데이터(필수는 아님)
ellipse.type = "convex", # 군집을 볼록 껍질(convex hull)로 표시
ggtheme = theme_bw() # 그래프 테마
)
ggsave("uns_hybrid_cluster_k5_plot.png", uns_hybrid_cluster_k5, width = 10, height = 10)
uns_hybrid_cluster_k5
load("pam_result_k3.rda")
# '정답' 레이블 로드
# 앞서 docvars()로 Corpus에 저장해둔 'true_topic' 메타데이터를 가져옴.
true_topics <- docvars(uns_corpus, "true_topic")
# '모델 예측(k=3)' 레이블 로드
# pam() 모델링 결과(k=3)에서 각 문서의 군집할당($clustering) 정보를 가져옴.
model_clusters_k3 <- pam_result_k3$clustering
# 혼동행렬(confusion matrix)로 비교
# R 기본함수 table()을 사용하여 '실제 정답'과 '모델의 예측'을 비교.
validation_table_k3 <- table(True = true_topics, Model = model_clusters_k3)
validation_table_k3
## Model
## True 1 2 3
## AS문의 314 617 69
## 배송 233 391 376
## 제품/사용문의 252 688 60
## 주문/결제 251 316 433
## 환불/반품/교환 546 344 110
k=3을 제안한 이유는, 모델이
5개의 주제 중 어휘적(TF-IDF)으로 매우 유사한 주제들을 하나의
의미 그룹으로 합쳤기 때문임. \(\Rightarrow\) 이는 모델의 ’실패’가 아니라,
인간이 5개로 나눈 주제들이 실제로는 3개의 큰 어휘적
덩어리로 구성되어 있음을 발견한 것임.Model)의 정체성 파악환불/반품/교환(546건) 주제를 압도적으로
포함하고 있음.AS, 배송,
제품, 주문)도 200~300건씩 이 군집에
포함되는데, 이는 이 주제들이 ‘환불’과 관련된 어휘([EX] ’금액’, ‘결제’,
‘취소’)를 공유하는 하위 그룹을 가지고 있음을 의미.환불/반품/교환을 핵심으로 하는
환불 및 금전 처리 관련 어휘 군집.제품/사용문의(688건)와
AS문의(617건)가 강력하게 결합된 가장 큰 덩어리.AS문의와
제품문의 모두에서 사용되어, 모델이 이 둘을 하나의
주제(“제품지원”)로 인식하고 있음을 명확히 보여줌.제품문의과 AS문의를
구분하지 못하는 제품 및 AS 지원 어휘군집.주문/결제(433건)와 배송(376건)이
짝을 이루고 있음.True)의 분포 파악환불/반품/교환(Row 5)
제품/사용문의(Row 3)
AS문의와 함께
“제품/AS” 군집을 정의하는 두 번째 ’닻’임.주문/결제(Row 4)
배송과 함께
“거래/물류” 군집을 정의하는 세 번째 ’닻’임.AS문의(Row 1): 하이브리드 주제
AS문의는
제품/사용문의와 어휘적으로 거의 구분이 불가능하며, 사실상
‘제품/AS’ 군집의 강력한 하위주제임을 의미.AS문의 중 상당수(약 30%)가 “수리비 환불”, “고장으로 인한
환불” 등 금전 문제와 결합된 하이브리드 속성을 가지고 있음을 나타냄.배송(Row 2): 허브 주제
주문/결제와 묶여 “순수 물류”([EX] “배송
출발”).제품/AS와 묶임 ([EX] “제품/AS부품 배송
문의”).환불과 묶임([EX] “배송지연으로 인한
환불”).k=3)가 정답(k=5)과 달랐던
이유: 환불(1번), 제품(2번),
주문(3번)이라는 3개의 큰 어휘적 ‘대륙’이 존재하며,
AS문의는 ’제품’(2번) 대륙에 속한 큰 ‘주(state)’이고,
배송은 이 모든 대륙을 연결하는 ’허브(hub)’ 공항과 같은
역할을 하고 있음.k=5로 강제 실행해보거나 TF-IDF 외에 다른 피처(feature)를
추가하는 것을 고려해야 함.AS문의와
제품/사용문의가 합쳐진 것은, 두 주제가 사용하는 단어의
풀(pool)이 매우 유사하기 때문. 모델은 ‘고장’, ‘작동’, ‘기사’, ‘확인’,
‘서비스’ 같은 단어들만 보고는 두 주제를 구분할 ’결정적인 증거’를 찾지
못한 것.AS문의와
제품/사용문의는 대화의 목적과 흐름(pragmatic
flow)이 다를 것.AS문의는 ‘불만’이나 ’요청’(‘부탁하기’,
‘명령하기/요구하기’)의 비율이 높을 것.제품/사용문의는 순수한 ’질문하기’의 비율이 높을
것.주문/결제는 ‘진술하기’(확인)의 비율이 높을 것.tfidf_matrix와는 별개로, 각
문서(대화)별로 화자(A/B)와 화행을 조합한 빈도 행렬
만들기([EX] ‘A_질문하기’, ‘B_질문하기_B’, ‘A_부탁하기_A’, ‘B_부탁하기’…
등을 열로 가짐).speech_act_matrix(문서 x 화행)와 기존
tfidf_matrix(문서 x 단어)를 cbind()로 합쳐서
하이브리드 피처 행렬을 만듦.tfidf_matrix와
speech_act_matrix는 값의 범위(scale)가 다르므로,
cbind()로 합친 최종 행렬을 scale() 함수로
표준화한 뒤 pam()과 PCA()에 넣을 것!pam()은 ‘거리’ 기반
알고리즘. TF-IDF 행렬에는 ‘환불’처럼 비교적 드물게 등장하지만 일단
등장하면 TF-IDF 값이 매우 높게 튀는 ’킬로미터(km)’ 단위의 피처([EX] 값의
범위가 0.0~5.0)와 ‘배송’처럼 더 자주 등장하지만 IDF가 낮아져 TF-IDF 값이
상대적으로 낮고 고르게 분포하는 ’센티미터(cm)’ 단위의 피처([EX] 값의
범위가 0.0~0.5)가 섞여 있음.load("pam_result_k3_scaled.rda")
# '정답' 레이블 로드
# 앞서 docvars()로 Corpus에 저장해둔 'true_topic' 메타데이터를 가져옴.
true_topics <- docvars(uns_corpus, "true_topic")
# '모델 예측(k=3)' 레이블 로드
# pam() 모델링 결과(k=3)에서 각 문서의 군집할당($clustering) 정보를 가져옴.
model_clusters_k3_scaled <- pam_result_k3_scaled$clustering
# 혼동행렬(confusion matrix)로 비교
# R 기본함수 table()을 사용하여 '실제 정답'과 '모델의 예측'을 비교.
validation_table_k3_scaled <- table(True = true_topics, Model = model_clusters_k3_scaled)
validation_table_k3_scaled
## Model
## True 1 2 3
## AS문의 812 125 63
## 배송 665 335 0
## 제품/사용문의 635 365 0
## 주문/결제 634 363 3
## 환불/반품/교환 778 222 0
k=3이 최적이라고 나옴. 하지만 Y축의 값을
보면 +0.008임. 0과 다름없는 이 점수는 사실상 ’구조 없음’을
의미. 이 플롯이 k=3을 ’최적’이라고 한 이유는 ’거대
덩어리(k=1)’에서 ’이상치 2개(k=3)’를 떼어내는 것이 0.008만큼 더 낫다고
수학적으로 알려준 것일 뿐.pam() 테이블과 실루엣 플롯 둘 다 이 데이터는
한 개의 덩어리와 소수의 이상치로 구성되어 있다는 동일한 진실을 말하고
있었음.load("pam_result_k5.rda")
# '정답' 레이블 로드
# 앞서 docvars()로 Corpus에 저장해둔 'true_topic' 메타데이터를 가져옴.
true_topics <- docvars(uns_corpus, "true_topic")
# '모델 예측(k=5)' 레이블 로드
# pam() 모델링 결과(k=5)에서 각 문서의 군집할당($clustering) 정보를 가져옴.
model_clusters_k5 <- pam_result_k5$clustering
# 혼동행렬(confusion matrix)로 비교-
# R 기본함수 table()을 사용하여 '실제 정답'과 '모델의 예측'을 비교.
validation_table_k5 <- table(True = true_topics, Model = model_clusters_k5)
validation_table_k5
## Model
## True 1 2 3 4 5
## AS문의 258 514 179 37 12
## 배송 184 342 46 295 133
## 제품/사용문의 243 665 5 54 33
## 주문/결제 190 243 87 354 126
## 환불/반품/교환 479 307 17 67 130
pam() 알고리즘에게 “군집 5개를 만들라”고 강제로 명령했지만,
TF-IDF로 계산된 데이터의 실제 구조는 3개의 거대한 덩어리로 뭉치려는 힘이
너무 강했음. 특히 k=3에서 보았던 강력한 어휘적 결합(특히
제품/AS)을 깨뜨리지 못했음. 알고리즘은 이 두 명령 사이에서
“타협”을 한 것.환불/반품/교환(479건)을 중심으로 한
“환불” 군집.제품/사용문의(665건)와
AS문의(514건)가 묶인 “제품/AS” 군집.주문/결제(354건)와 배송(295건)이
묶인 “거래/물류” 군집.k=3에서 봤던 3개의 핵심구조가 Model 1, 2, 4로 이름만
바뀐 채 그대로 나타남.AS문의(179건)의 일부를, Model 5는
배송(133건)과 주문(126건)의 일부를 가져가며
의미 있는 주제군집을 형성하는 데 실패.k=5 결과는 실루엣 계수가 k=3이라고 한
것이 옳았음을 증명함.k=5라는 명령에도 불구하고, 어휘적으로
가장 강력한 3개의 덩어리를 그대로 유지함.load("pam_result_hybrid.rda")
load("true_topics_hybrid.rda")
# '정답' 레이블 로드
true_topics_hybrid
# '모델 예측(k=5)' 레이블 로드
# pam() 하이브리드 모델링 결과(k=5)에서 각 문서의 군집할당($clustering) 정보를 가져옴.
model_hybrid_clusters_k5 <- pam_result_hybrid$clustering
# 혼동행렬(confusion matrix)로 비교
# R 기본함수 table()을 사용하여 '실제 정답'과 '모델의 예측'을 비교.
validation_hybrid_table_k5 <- table(True = true_topics_hybrid, Model = model_hybrid_clusters_k5)
print("Validation validation_hybrid_table_k5(True vs. Model):")
validation_hybrid_table_k5
# [결과]
# Model
# True 1 2 3 4 5
# AS문의 209 345 314 63 2
# 배송 225 517 197 0 0
# 제품/사용문의 52 810 88 0 0
# 주문/결제 130 648 190 2 0
# 환불/반품/교환 194 530 214 0 0
A_진술하기, A_인사하기)를
성공적으로 제거했지만, 모델은 곧바로 2순위로 가장 흔한
공통점을 찾아내 그것을 기준으로 새로운 쓰레기통 군집(Model 2)을
만듦. \(\Rightarrow\) 이는 현재 우리가
가진 ‘어휘’와 ’화행’ 피처만으로는 5개 주제를 분리할 ’결정적인
신호(signal)’가 부족하다는 강력한 증거임.AS문의가 성공적으로 분리되었으나,
나머지 4개 주제가 ’단순대화’라는 새로운 공통점으로 묶이게 됨.제품/사용문의(810건),
주문/결제(648건), 환불/반품/교환(530건),
배송(517건) 등 AS문의를 제외한 모든 주제의
과반수를 흡수.AS문의를 제외한 4개 주제가
유사한 ’대화 패턴’을 공유한다는 것을 발견했음을 의미.A_진술하기 등)를
제거했음에도 불구하고, “단순질문 \(\rightarrow\) 단순답변” 또는 “정보 확인
\(\rightarrow\) 처리”와 같이 남아 있는
A_질문하기 등과 패턴이 매우 유사할 수 있음. Model 2는 이
“표준적인 고객응대 패턴”을 가진 문서들을 모두 묶은 것.Model 1: AS문의(209),
배송(225), 환불(194).Model 3: AS문의(314),
배송(197), 환불(214).AS문의와 환불/배송 주제가
섞여 있음. 이는 이 3개 주제가 ’문제해결’이라는 속성을 공유하며 여전히
명확하게 분리되지 못하고, 억지로 k=5를 맞추기 위해
파편화되었음을 보여줌.AS문의는 유일하게 Model 2(345건)에 쏠리지 않고, Model
1(209건)과 Model 3(314건)으로 성공적으로 분리.배송과 환불 주제의 일부(각각 200건 내외)도
이 군집들로 따라옴.AS문의가
제품/사용문의와는 대화 패턴이 완전히 다르다는 것을 감지.
\(\Rightarrow\) AS문의는
‘단순질문’이 아닌 ’문제제기’, ‘불만’, ‘요청’ 등 “복잡하고 비표준적인
대화 패턴”을 가지며, 모델은 이 패턴을 Model 1과 3으로 분리해낸 것.AS문의 63건, 2건 등을 가져가는 등,
통계적으로 거의 비어있는 “먼지” 군집이 되었음. \(\Rightarrow\) PAM 알고리즘이 5개의 의미
있는 중심점(medoid)을 찾는 데 완전히 실패했음을 보여줌.k=3 TF-IDF Only 결과에서 봤듯이,
제품/AS와 주문/배송은 어휘적으로 매우
유사.A_진술하기, A_인사하기 등을
뺐더니, 남은 ‘신호’ 화행들([EX] B_부탁하기,
B_부정감정표현하기)마저 여러 주제에 걸쳐 공통적으로
나타났을 가능성이 큼([EX] AS문의와
환불문의 둘 다 부정감정표현하기를
사용함).제품/AS 분리라는
가장 어려운 과제를 성공적으로 해냄(제품은 Model 2로,
AS는 Model 1/3으로). 하지만 그 대가로 ’화행 패턴’이
유사했던 나머지 4개 주제가 Model 2라는 하나의 거대 군집으로 합쳐지는
’쏠림 현상’이 발생. 이는 모델이 이제 ’어휘’가 아닌 ’대화의
복잡성/패턴’을 기준으로 데이터를 재편성했음을 보여주는 매우 흥미로운
결과임.[그림 4] 데이터 주위를 돌면서 가장 적절한 관찰방향
찾기
[그림 5] 차원축소 예시(2차원에서 1차원으로): 데이터 겹침의
문제 발생
[그림 6] 차원축소 예시(2차원에서 1차원으로): 분산을 잘
보존하는 방식으로 차원 축소하기
[그림 7] 차원축소와 주성분(PC)
PCA() 함수가 이 모든
것을 자동으로 처리함. 하지만 ’어떤 원리’로 축을 찾는지 이해하면 PCA를 더
깊이 있게 활용할 수 있음.fviz_eig() \(\Rightarrow\) 몇 개의 축을 사용할
것인가?[그림 8] scree 플롯 예시
+와 -PC1 점수를 계산하는 실제 공식(레시피)은
+/- 부호가 있는 ‘가중치(weight)’(정확히는
‘로딩’ 또는 ‘고유 벡터’)를 사용.+ 방향(양의 극성): ‘환불’, ‘입금’ 단어들이 이 축의
양(+)의 방향을 정의.- 방향(음의 극성): ‘배송’, ‘출발’ 단어들이 이 축의
음(-)의 방향을 정의.fviz_contrib(): PC 축에 ‘이름 붙이기’.fviz_contrib()는 이 계수(로딩)의 ‘제곱값’(부호를
무시하고 순수 기여량만 봄)을 시각화하여, 어떤 단어가 이 축을 ’형성’하는
데 크게 기여했는지 한눈에 보여줌.+/- 부호를 그대로 사용해서 중요도를 더하면, \((+0.5) + (-0.5) = 0\)이 되어버려서 ’환불’과
’배송’이 하나도 안 중요한 단어처럼 보이는 심각한 왜곡이 발생.fviz_pca_ind(): 해석의 ‘신뢰도’ 평가.PCA()prcomp()가 단순 ’계산’에
중점을 둔다면, PCA()는 앞서 이론 파트에서 다룬 ’해석’에
필요한 모든 자료를 한 번에 생성해줌. tfidf_matrix를
입력받아 PCA 결과 객체를 생성함.X: 숫자 행렬 또는 데이터프레임(본 수업에서는
tfidf_matrix가 입력됨).scale.unit: TRUE로 설정 시, PCA 수행 전
모든 변수(단어)를 평균 0, 분산 1로 표준화(scaling)함. TF-IDF 행렬이라도
각 단어(열)의 분산이 다를 수 있으므로, 모든 단어가 동등한 영향력을
갖도록 TRUE로 설정하는 것이 일반적임.ncp: 보존(계산)할 주성분의 최대 개수(기본값 5).graph: TRUE로 설정 시
FactoMineR 고유의 그래프를 출력함. 우리는
factoextra의 ggplot2 기반 그래프를
사용할 것이므로 FALSE로 설정함.fviz_eig()X: PCA() 결과 객체(본 수업에서는
pca_result가 입력됨).addlabels: TRUE로 설정 시 막대 위에
분산(%) 값을 텍스트로 표시함.ncp: 상위 몇 개의 PC까지 그래프에 그릴지 지정함([EX]
10).fviz_contrib()X: PCA() 결과
객체(pca_result).choice: "var"(변수/단어의 기여도) 또는
"ind"(개별 문서의 기여도)를 볼지 선택.axes: 기여도를 확인할 PC 축 번호(정수)([EX]
1, 2)top: 기여도가 가장 높은 상위 몇 개만 표시할지 지정함.
([EX] 10)fviz_pca_var() 및 fviz_pca_ind()_var) 또는
개별 관측치(_ind)를 시각화함.fviz_pca_var(): 단어들 간의 관계([EX] ’배송’과 ’출발’이
같은 방향인지)와 축 기여도를 한눈에 봄.fviz_pca_ind(): 문서들의 분포를 ’의미지도’처럼
시각화함.fviz_pca_var() 함수(변수/단어 플롯) 주요 논항X: PCA() 결과
객체(pca_result).col.var: 변수(단어)의 색상을 무엇을 기준으로 칠할지
지정.
"contrib": 기여도(축 형성에 중요한 단어 강조)."cos2": 표현품질(2D 평면에 잘 표현된 단어 강조).repel: TRUE로 설정 시 텍스트가 겹치지 않게
함(필수).fviz_pca_ind()(개별/문서 플롯) 주요 논항X: PCA() 결과
객체(pca_result).col.ind
"cos2": 표현품질(2D 위치가 신뢰할 만한지 평가).habillage
true_topic_factor)를 이 논항에
전달하면, 실제 주제별로 점의 색상을 다르게 칠해줌.addEllipses: TRUE로 설정 시
habillage로 칠해진 그룹별로 타원을 그려줌.load("pca_result.rda")
# 관련 패키지 로드
library(FactoMineR) # PCA() 함수 제공.
# PCA 실행
# FactoMineR 패키지의 PCA() 함수를 사용하여 PCA 실행.
pca_result <- PCA(
tfidf_matrix , # 입력 데이터(숫자 행렬): 기본 매트릭스 적용.
scale.unit = FALSE, # 이미 앞서 tfidf_matrix 생성 시 표준화를 적용했으므로 다시 표준화를 적용할 필요 없음.
graph = FALSE # FactoMineR 자체 그래프는 끔(factoextra 사용).
)
save(pca_result, file="pca_result.rda")
# scree 플롯 시각화
# fviz_eig: PCA 결과(pca_result)의 고윳값(eigenvalue)을 시각화.
uns_eig <- fviz_eig(
pca_result, # PCA 결과 객체.
addlabels = TRUE, # 막대 위에 분산(%) 표시.
ncp = 7 #상위 7개 PC만 표시.
)
ggsave("uns_eig_plot.png", uns_eig, width = 10, height = 10)
uns_eig
uns_pca_va_plot.png(변수 플롯)과
uns_pca_ind_plot.png(개별 문서 플롯)은 데이터의 1%만을
보여주는 극히 일부의 ’그림자’임을 인지해야 함. 이 2D 플롯에서 겹쳐
보인다고 해서, 실제 수천 차원의 공간에서 겹쳐 있다고 단정할 수
없음.# 단어 기여도 시각화(PC1)
# fviz_contrib(): PCA 축 형성에 '기여'한 변수(단어)들을 시각화해줌.
uns_pc1_contrib <- fviz_contrib(pca_result, # PCA 결과 객체.
choice = "var", # '변수(variable)'의 기여도를 보겠음(반대: "ind").
axes = 1, # 'PC1' 축에 대한 기여도.
top = 10 # 기여도가 가장 높은 상위 10개 단어만 표시.
)
ggsave("uns_pc1_contrib_plot.png", uns_pc1_contrib, width = 10, height = 10)
uns_pc1_contrib
# 단어 기여도 시각화(PC2)
uns_pc2_contrib <- fviz_contrib(pca_result,
choice = "var",
axes = 2,
top = 10)
ggsave("uns_pc2_contrib_plot.png", uns_pc2_contrib, width = 10, height = 10)
uns_pc2_contrib
기사(technician), 방문(visit),
점검(inspection), 출장(business trip),
안(e.g., 작동이 ‘안’ 됨) 등은 모두 “AS(수리/고장)” 상황과
직접적으로 관련된 어휘임.거, 한, 같, 번
등은 토크나이저가 분리한 형태소로, ‘고장 난 거 같아요’,
’한 번 봐주세요’처럼 AS 관련 맥락에서 자주 등장하여
함께 뽑힌 것으로 보임.만족도, 조사, 개인,
동의, 제공, 취급,
방침: 이는 상담원이 고객에게 ’개인정보 동의’를 구하거나,
’만족도 조사’를 안내하는 등의 상담 후속조치 및 정보처리 맥락의
어휘임.# 변수(단어) 상관 원형 플롯
uns_pca_va <- fviz_pca_var(pca_result,
col.var = "contrib", # 단어 색상을 '기여도'에 따라 매핑.
gradient.cols = c("#00AFBB", "#E7B800", "#FC4E07"), # 기여도 색상표.
repel = TRUE # 단어 텍스트가 겹치지 않도록 함.
)
ggsave("uns_pca_va_plot.png", uns_pca_va, width = 10, height = 10)
[그림 9] 변수 상관 원형 플롯